using System;
using System.Collections.Generic;
using Enyim.Caching;
using Enyim.Caching.Memcached;
using System.Collections;
using log4net;
using Castle.ActiveRecord;
using Castle.ActiveRecord.Framework.Config;
using NHibernate.Mapping;

namespace AgileJedi.MemcachedForMonorail.CustomSessionStateExtensions
{
	/// <summary>
	/// Implements a Specialized Idictionary for use in Monorail Sessions
	/// 
	/// </summary>
	/// <remarks>Uses a dictionary to store data on the backend. Once the persist method is called then the values are written 
    /// to the memcached store and cleared from the dictionary.</remarks>
	/// <seealso cref="MemCacheSessionFactory"/>
	public class MemCachedSessionDictionary : IDictionary, IDisposable
	{
		/// <summary>
		/// Name of the default config section to use
		/// </summary>
		public const string STR_Sessionstate = "sessionstate";

		#region Fields
		private MemcachedClient _mcClient;
		private Guid _sessionId;
		private ILog _logger = LogManager.GetLogger(typeof(MemCachedSessionDictionary));
		private List<string> _changes = new List<string>();
		private bool _useDatabase = true;
		private bool _useActiveRecord = true;
		private static MemCacheDB rawDBWriter = new MemCacheDB();
		private static Boolean _arDBInitialized = false;
		private static Boolean _rawDBInitialized = false;
        private IDictionary _tempDictionary = new Hashtable();
        private IList _keysToBePersisted = new System.Collections.ArrayList();
		#endregion

		#region Constructors

		/// <summary>
		/// Initializes a new instance of the MemCachedSession class.
		/// </summary>
		/// <param name="sessionId">The unique ID for the session being initialized from memcached </param>
		public MemCachedSessionDictionary(Guid sessionId)
			: this(sessionId, STR_Sessionstate)
		{


		}

		public MemCachedSessionDictionary(Guid sessionId, string configSection)
			: this(sessionId, new Enyim.Caching.MemcachedClient(configSection))
		{

		}

		public MemCachedSessionDictionary(Guid sessionId, MemcachedClient client)
		{
			_sessionId = sessionId;
			_mcClient = client;
			this._SessionTimeToLiveMinutes = 20;
		}

		#endregion

        public override string ToString()
        {
            return String.Format("Memcached Session [{0}] with {1} keys to be persisted.", _sessionId, _keysToBePersisted.Count);
        }

		public System.Collections.IList  GetChangedFields()
		{
			return _keysToBePersisted;
		}

        /// <summary>
        /// Write all temp values to memcached and clears the
        /// temporary dictionary.
        /// </summary>
        public void persistValues()
        {
            foreach(string key in _keysToBePersisted)
            {
                if (!_tempDictionary.Contains(key))
                {
                    continue;
                }                
            	this.WriteMemCachedValue(key, _tempDictionary[key]);
			    if (this._useDatabase)
                    PersistItem(key, _tempDictionary[key]);
            }            
            _keysToBePersisted.Clear();
    
        }

		#region IDictionary Members

		bool IDictionary.IsFixedSize
		{
			get { return false; }
		}

		public IDictionaryEnumerator GetEnumerator()
		{
			throw new Exception("The method or operation is not implemented.");
		}

        /// <summary>
        /// Adds or replaces an item in the session dictionary.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <remarks>All writing to the session should come through this method. It tracks which items have changed and need to be persisted
        /// to memcached.</remarks>
		public void Add(object key, object value)
		{
			if (value == null)
			{
				this.Remove(key);
				return;
			}

			this._tempDictionary[key] = value;
            if (!this._keysToBePersisted.Contains(key))
            {
                _keysToBePersisted.Add(key);
            }
		}

		public bool ContainsKey(object key)
		{
			return ((_tempDictionary.Contains(key) && _tempDictionary[key] != null) || this[key] != null);

		}


		public void Remove(object key)
		{
			try
			{
				this.RemoveMemCachedValue(key.ToString());
                if (this._useDatabase)
                {
                    this.RemoveDBValue(key.ToString());
                }
                this._tempDictionary.Remove(key);

			}
			catch (Exception ex)
			{
				_logger.Warn("Exception occurred whle trying to remove from memcached", ex);
				return ;
			}

			return;
		}

		public bool TryGetValue(object key, out object value)
		{
			value = null;
			try
			{
				value = this[key];
				return value != null;
			}
			catch
			{
				return false;
			}


		}

		/// <summary>
		/// This property is not currently supported
		/// </summary>
		public ICollection Values
		{
			get { throw new NotSupportedException("The method or operation is not implemented."); }
		}
		/// <summary>
		/// Retrieve key values
		/// </summary>
		/// <param name="key">the of key of the value to be returned</param>
		/// <returns>Returns the value associated with the provided <paramref name="key"/>.
		/// For keys that do not exist a <see cref="null"/> value will be returned.</returns>
        /// <remarks>Most of the heavy lifting is done here. When values are retrieved they
        /// are placed in both memcached and the local tempDictionary. The local temp dictionary works because we can
        /// assume that the client is connected to only one node at a time.</remarks>
		public object this[object key]
		{
			get
			{
				
                if (this._tempDictionary.Contains(key))
                {
                    return _tempDictionary[key];
                }

                object returnValue;
                returnValue = GetMemCachedValue(key);

				if (returnValue == null){					
				
					returnValue = RetrievePersistedItem(key.ToString());
					if (returnValue != null)
						this.WriteMemCachedValue(key, returnValue);					
				}

                ///
                /// Persist the value to the tempDictionary
                ///
                _tempDictionary[key] = returnValue;
                return returnValue;


			}
			set
			{
				this.Add(key, value);
			}
		}

       		#endregion

		#region ICollection Members

		public ICollection Keys
		{
			get
			{

				//					List<string> keysList = _mcClient.Get<List<string>>(_sessionId + ":sessionkeys");
				//					if (keysList == null)
				//					{
				//						keysList = new List<string>();
				//						UpdateKeys(keysList);
				//					}
				//					return keysList;
				throw new NotSupportedException("Support for the Keys collection may be added later...at this time it is too costly to track keys.");
			}
		}
		
		public void Clear()
		{
            this._tempDictionary.Clear();
            this.IncrementRevision();
			if (_useDatabase)
			{
				if (_useActiveRecord)
					ARSessionItem.RemoveSession(this._sessionId);
				else
					rawDBWriter.RemoveSession(this._sessionId);
			}
		}

		public bool Contains(object key)
		{
			return this.ContainsKey(key);
		}


        /// <summary>
        /// For performance reasons we done implement this method!
        /// </summary>
        /// <param name="array"></param>
        /// <param name="arrayIndex"></param>
		public void CopyTo(System.Array array, int arrayIndex)
		{
			throw new NotSupportedException("The method or operation is not implemented.");
		}

		object ICollection.SyncRoot
		{
			get { return null; }
		}

		bool ICollection.IsSynchronized{

			get { return true; }
		}

		public int Count
		{
			get { throw new NotSupportedException("The method or operation is not implemented."); }
		}

		public bool IsReadOnly
		{
			get { return false; }
		}
		

		#endregion

		#region IEnumerable Members

		IEnumerator IEnumerable.GetEnumerator()
		{
			throw new Exception("The method or operation is not implemented.");
		}

		#endregion

		#region Properties

		/// <summary>
		/// Used inernally to get the key that the current revision 
		/// number is stored under
		/// </summary>
		public string RevisionKey
		{
			get { return String.Format("session.{0}.rev", _sessionId); }
		}

		public int GetRevision()
		{
			return _mcClient.Get<int>(this.RevisionKey);
		}
		public Boolean UseDatabase
		{
			get { return this._useDatabase; }
			set { this._useDatabase = value; }
		}

		public bool UseActiveRecord
		{
			get { return _useActiveRecord; }
			set { _useActiveRecord = value; }
		}

		private double _SessionTimeToLiveMinutes;
		public double SessionTimeToLiveMinutes
		{
			get { return _SessionTimeToLiveMinutes; }
			set { _SessionTimeToLiveMinutes = value; }
		}
		#endregion

		#region Private Members

		private string GetMemCachedKey(string sessionKey)
		{
			return string.Format("session.{0}.rev.{1}.{2}", _sessionId, GetRevision(), sessionKey);
		}

		/// <summary>
		/// Used internally to increment the session revision. The Revision
		/// is incremented when ever the session is cleared. This revision is 
		/// used to simulate namespaces. Incrementing the revision simulates
		/// the session being cleared.
		/// </summary>
		private void IncrementRevision()
		{
			_mcClient.Store(StoreMode.Add, this.RevisionKey, 1, TimeSpan.FromMinutes(this.SessionTimeToLiveMinutes + 15));
			long newRevision = _mcClient.Increment(this.RevisionKey, 1);
			_logger.InfoFormat("New Revision {0} for session {1}", newRevision.ToString(), _sessionId);
		}

		private object GetMemCachedValue(object key)
		{
			return _mcClient.Get(GetMemCachedKey(key.ToString()));
		}

		private void WriteMemCachedValue(object key, object value)
		{
			_mcClient.Store(StoreMode.Set, GetMemCachedKey(key.ToString()), value, GetTimeToLive());
		}

		private void RemoveMemCachedValue(string key)
		{
			_mcClient.Remove(GetMemCachedKey(key));
		}

		private TimeSpan GetTimeToLive()
		{
			return TimeSpan.FromMinutes(this.SessionTimeToLiveMinutes);
		}
		#endregion

		#region DB Persistence Methods

		/// <summary>
		/// Initializes ActiveRecord's connection to the database.
		/// </summary>
		/// <param name="path">Path to an ActiveRecord config file.</param>
		public static void SetupActiveRecordDBConnection(string path)
		{
			if (!_arDBInitialized)
			{
				

				ActiveRecordStarter.Initialize(System.Reflection.Assembly.GetAssembly(typeof(MemCacheSessionFactory)),
				 new XmlConfigurationSource(path));
				ActiveRecordStarter.UpdateSchema();
				_arDBInitialized = true;
				
			}
		}
		ARSessionItem _persistedItem = new ARSessionItem();
		/// <summary>
		/// Persists the item to the AR layer
		/// </summary>
		/// <param name="key"></param>
		/// <param name="item"></param>
		private void PersistItem(string key, object item)
		{
				if (_useActiveRecord)
					ARPersistItem(key, item);
				else
					RawPersistItem(key, item);
		}

		private void ARPersistItem(string key, object value)
		{
			if (value == null) return;

			ARSessionItem sessItem = ARSessionItem.Find(_sessionId, key);

			try
			{
				if (sessItem != null)
				{
					sessItem.Value = value;
					ActiveRecordMediator.Update(sessItem);
				}
				else
				{
					sessItem = new ARSessionItem(_sessionId, key, value);

					ActiveRecordMediator.Create(sessItem);
				}
			}
			catch (Exception ex)
			{
				_logger.Fatal(String.Format("Failed to Persist Item {0} with value {1} to ActiveRecord", key, value), ex);

				throw new Exception(String.Format("Failed to Persist Item {0} with value {1} to ActiveRecord", key, value), ex);
			}
						
		}

		private void RawPersistItem(string key, object item)
		{
			if (item == null) return;
			rawDBWriter.PersistRecord(_sessionId, key, item);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="key"></param>
		private void RemoveDBValue(string key)
		{
			if (_useActiveRecord)
				ARRemoveItem(key);
			else
				RawRemoveItem(key);
		}

		private void ARRemoveItem(string key)
		{
			ARSessionItem arItem = ARSessionItem.Find(_sessionId, key);
			if (arItem != null)
				arItem.Remove();
		}

		private void RawRemoveItem(string key)
		{
			rawDBWriter.RemoveRecord(_sessionId, key);
		}

		/// <summary>
		/// Retrieves the item persisted in the ActiveRecord backend
		/// </summary>
		/// <param name="key">The key of the item to retrieve </param>
		/// <returns>Returns the item associated with the key. Returns null when the item does not exist.</returns>
		private object RetrievePersistedItem(string key)
		{
			if (_useActiveRecord)
				return ARRetrieveItem(key);
			else
				return RawRetrieveItem(key);
		}

		private object ARRetrieveItem(string key)
		{
			ARSessionItem tempItem = ARSessionItem.Find(_sessionId, key);
			return tempItem != null ? tempItem.Value : null;
		}

		private object RawRetrieveItem(string key)
		{
			return rawDBWriter.RetrieveRecord(_sessionId, key);
		}
		#endregion

		public static void CloseActiveRecordDBConnection()
		{
			ActiveRecordStarter.ResetInitializationFlag();
			_arDBInitialized = false;
		}


		public static void SetupRawDBConnection()
		{
			if (!_rawDBInitialized)
			{
				rawDBWriter.SetupConnection();
				_rawDBInitialized = true;
			}
		}

		public static void CloseRawDBConnection()
		{
			rawDBWriter.CloseConnection();
			ActiveRecordStarter.ResetInitializationFlag();
			_rawDBInitialized = false;
		}

        #region IDisposable Members

        public void Dispose()
        {
            this.persistValues();
            this._tempDictionary.Clear();
            this._keysToBePersisted.Clear();
        }

        #endregion
    }
}
